//the underlying storage engine for the block store
package chunkstore

import (
	"errors"
	"os"
	"strconv"
	"syscall"
	"unsafe"
)

const (
	DefaultChunkSize = 1 * 1024 * 1024 * 1024
)

const (
	//not change the file size
	FALLOC_FL_KEEP_SIZE = 1 + iota
	FALLOC_FL_PUNCH_HOLE
)

type ChunkInfo struct {
	Fd        int
	Capacity  int64
	ChunkCont []byte
}

type Store struct {
	dataDir       string
	chunkPointers map[int64]*ChunkInfo
}

func NewStore(dir string) (s *Store, err error) {
	s = &Store{dataDir: dir, chunkPointers: make(map[int64]*ChunkInfo)}

	return s, FsyncDir(s.dataDir)
}

func FsyncDir(dir string) (err error) {
	fp, err := syscall.Open(dir, os.O_RDONLY, 0666)
	if err != nil {
		return
	}
	if err = syscall.Fsync(fp); err != nil {
		return
	}

	return
}

//create and allocate blocks
func (s *Store) CreateChunk(chunkId, size int64) (err error) {
	if size <= 0 {
		size = DefaultChunkSize
	}

	fp, err := os.OpenFile(s.dataDir+"/"+strconv.Itoa(int(chunkId)), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
	if err != nil {
		return
	}
	fd := fp.Fd()
	if err = syscall.Fallocate(int(fd), FALLOC_FL_KEEP_SIZE, 0, size); err != nil {
		return
	}
	if err = syscall.Ftruncate(int(fd), size); err != nil {
		return
	}
	chunkCont, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
	if err != nil {
		return err
	}
	s.chunkPointers[chunkId] = &ChunkInfo{Fd: int(fd), Capacity: size, ChunkCont: chunkCont}
	//fp.Close()

	return
}

//no need sync
func (s *Store) BufferedWrite(chunkId, offset int64, data []byte) error {
	chunk, err := s.getChunk(chunkId)
	if err != nil {
		return err
	}
	length := int64(len(data)) + offset
	if chunk.Capacity < length*1 || offset < 0 {
		return errors.New("ChunkSizeMismatchError")
	}
	copy(chunk.ChunkCont[offset:length], data)

	return nil
}

func (s *Store) SyncWrite(chunkId, offset int64, data []byte) error {
	chunk, err := s.getChunk(chunkId)
	if err != nil {
		return err
	}
	length := int64(len(data)) + offset
	if chunk.Capacity < length*1 || offset < 0 {
		return errors.New("ChunkSizeMismatchError")
	}
	copy(chunk.ChunkCont[offset:length], data)
	if err := msync(chunk.ChunkCont, syscall.MS_ASYNC); err != nil {
		return err
	}

	return nil
}

func (s *Store) Fetch(chunkId, offset, size int64) (data []byte, err error) {
	chunk, err := s.getChunk(chunkId)
	if err != nil {
		return nil, err
	}
	length := offset + size
	if chunk.Capacity < length || offset < 0 || size < 0 {
		return nil, errors.New("ChunkSizeMismatchError")
	}
	data = make([]byte, size)
	copy(data, chunk.ChunkCont[offset:length])

	return data, nil
}

//overwrite as zero
func (s *Store) Erase(chunkId, offset, size int64) error {
	chunk, err := s.getChunk(chunkId)
	if err != nil {
		return err
	}
	length := offset + size
	if chunk.Capacity < length || offset < 0 || size < 0 {
		return errors.New("ChunkSizeMismatchError")
	}
	e := make([]byte, size)
	copy(chunk.ChunkCont[offset:length], e)

	return nil
}

func (s *Store) DeleteChunk(chunkId int64) error {
	chunk, err := s.getChunk(chunkId)
	if err != nil {
		return err
	}
	if err := syscall.Munmap(chunk.ChunkCont); err != nil {
		return err
	}
	// fp := os.NewFile(uintptr(chunk.Fd), strconv.Itoa(int(chunkId)))
	syscall.Close(chunk.Fd)
	delete(s.chunkPointers, chunkId)

	return nil
}

func msync(b []byte, flag int) error {
	var _p unsafe.Pointer
	if len(b) <= 0 {
		return errors.New("MmapEmptyError")
	}
	_p = unsafe.Pointer(&b[0])
	if _, _, errno := syscall.Syscall(syscall.SYS_MSYNC, uintptr(_p), uintptr(len(b)), uintptr(flag)); errno != 0 {
		return errors.New("SyscallMsyncError")
	}

	return nil
}

func (s *Store) getChunk(chunkId int64) (chunk *ChunkInfo, err error) {
	if s.chunkPointers == nil {
		return nil, errors.New("ChunkPointersEmptyError")
	}
	chunk, ok := s.chunkPointers[chunkId]
	if !ok {
		return nil, errors.New("ChunkIdNotExistError")
	}

	return chunk, nil
}

// func (s *Store) statChunkCapacity(chunkId int64) (capacity int64, err error) {
// 	var statInfo syscall.Stat_t

// 	err = syscall.Stat(s.dataDir+"/"+strconv.Itoa(int(chunkId)), &statInfo)
// 	if err != nil {
// 		return
// 	}

// 	return statInfo.Size, nil
// }
